package org.vaadin.nf4v.ext.config.afternav;

import com.vaadin.ui.Component;
import org.vaadin.nf4v.core.NavigableApplication;
import org.vaadin.nf4v.core.NavigationException;
import org.vaadin.nf4v.core.config.PageConfig;
import org.vaadin.nf4v.core.navigation.NavigationManager;
import org.vaadin.nf4v.core.navigation.NavigationRequest;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Utility class.
 * Provides access to after navigation listener method in page.
 */
public class ExtAfterNavUtil {

    /**
     * @param config page config
     * @return page method marked with @AfterNavigation annotation or null if no annotated method found
     */
    public static Method getAfterNavigationListenerMethod(PageConfig config) {
        return config.getAttribute(AfterNavigationAnnotationParser.AFTER_NAVIGATION_METHOD_KEY);
    }

    /**
     * @param config page config
     * @return type of parameters of method annotated with @AfterNavigation
     */
    public static MethodType getAfterNavigationListenerMethodParamType(PageConfig config) {
        return config.getAttribute(AfterNavigationAnnotationParser.AFTER_NAVIGATION_METHOD_PARAMS_KEY);
    }

    /**
     * @param config page config
     * @return true if instance reuse is supported by page, false otherwise
     */
    public static boolean isAfterNavigationListenerMethodAllowInstanceReuse(PageConfig config) {
        return config.getAttribute(AfterNavigationAnnotationParser.AFTER_NAVIGATION_METHOD_SAME_INSTANCE_KEY) == null;

    }

    /**
     * @param config page config
     * @return true if page class has method annotated with @AfterNavigation, false otherwise
     */
    public static boolean hasAfterNavigationListenerMethod(PageConfig config) {
        return getAfterNavigationListenerMethod(config) != null;
    }

    /**
     * @param request navigation request
     * @return true if instance reuse is supported by page, false otherwise
     * @throws NavigationException if configuration for page not found
     */
    public static boolean isAfterNavigationListenerMethodAllowInstanceReuse(NavigationRequest request)
            throws NavigationException {
        Class<? extends Component> pageClass = request.getTarget().getPageClass();
        String context = NavigationManager.getNavigationContext();
        PageConfig config = NavigableApplication.getCurrent().getNavigationConfig()
                .getPageConfig(context, pageClass);
        if (config == null) {
            throw new NavigationException("Page " + pageClass.getSimpleName() + " is not registered in context '" +
                    context + "'");
        }
        return isAfterNavigationListenerMethodAllowInstanceReuse(config);
    }

    /**
     * @param request navigation request
     * @return true if page class has method annotated with @AfterNavigation, false otherwise
     * @throws NavigationException if configuration for page not found
     */
    public static boolean hasAfterNavigationListenerMethod(NavigationRequest request)
            throws NavigationException {
        Class<? extends Component> pageClass = request.getTarget().getPageClass();
        String context = NavigationManager.getNavigationContext();
        PageConfig config = NavigableApplication.getCurrent().getNavigationConfig()
                .getPageConfig(context, pageClass);
        if (config == null) {
            throw new NavigationException("Page " + pageClass.getSimpleName() + " is not registered in context '" +
                    context + "'");
        }
        return hasAfterNavigationListenerMethod(config);
    }

    /**
     * Calls after navigation listener method on page instance.
     * Provides NavigationRequest or Params object as parameter if required.
     *
     * @param request current navigation request
     * @throws NavigationException if configuration for page not found or page has no annotated method
     */
    public static void notifyAfterNavigation(NavigationRequest request) throws NavigationException{
        String context = NavigationManager.getNavigationContext();
        Class<? extends Component> pageClass = request.getTarget().getPageClass();
        PageConfig config = NavigableApplication.getCurrent().getNavigationConfig()
                .getPageConfig(context, pageClass);
        if (config == null) {
            throw new NavigationException("Page " + pageClass.getSimpleName() + " is not registered in context '" +
                    context + "'");
        }
        if (!hasAfterNavigationListenerMethod(config)) {
            throw new NavigationException("No method annotated with @" + AfterNavigation.class.getSimpleName() +
                    " found for page " + pageClass.getSimpleName() + " in context '" + context + "'");
        }
        Method method = getAfterNavigationListenerMethod(config);
        boolean recoverAccessibility = false;
        try {
            if (!method.isAccessible()) {
                method.setAccessible(true);
                recoverAccessibility = true;
            }

            switch (getAfterNavigationListenerMethodParamType(config)) {
                case NO_PARAMS:
                    method.invoke(request.getPageInstance());
                    break;
                case PARAMS:
                    method.invoke(request.getPageInstance(), request.params());
                    break;
                case REQUEST:
                    method.invoke(request.getPageInstance(), request);
                    break;
                default:
                    throw new RuntimeException("Configuration error: can't determine parameters type for method " +
                            config.getPageClass().getSimpleName() + "#" + method.getName() + " in context '" + context + "'");
            }

        } catch (IllegalAccessException e) {
            throw new RuntimeException("Can't notify after navigation listener method " + method.getName(), e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Can't notify after navigation listener method " + method.getName(), e);
        } finally {
            if (recoverAccessibility) {
                method.setAccessible(false);
            }
        }
    }
}
